#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <fcntl.h>

#include "ramprakash.Part1Common.h"

typedef struct _user_record 
{
    char *u;
    char *p;
} user_t;


// Globals
struct sockaddr_in clnt;
int listenSockFd;
int clntSockFd;
fd_set readFds;

// User records - add to this to add new users - Need to store this encrypted
// to make it more secure - FIXME fast!!
static user_t users[] = { 
    {"guest", "password"},
    {"user", "password"},
    {"guest", "guest"},
    {"ee282", "nice282"},
    {NULL, NULL}
};
                 

//socket helper functions
int sendMsg(char *buf, int size)
{
    if (sendto(clntSockFd, buf, size, 0, NULL, 0) == -1) 
    {
        perror("sendto");
        return errno;
    }
    return 0;
}

int createListeningSocket()
{
    struct sockaddr_in servaddr;
    int optval = 1;
    int buflen = 2048;

    listenSockFd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSockFd < 0) 
    {
        perror("socket");
        return errno;
    }

    setsockopt(listenSockFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));
    setsockopt(listenSockFd, SOL_SOCKET, SO_SNDBUF, &buflen, sizeof(int));
    setsockopt(listenSockFd, SOL_SOCKET, SO_RCVBUF, &buflen, sizeof(int));


    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(LISTENING_ADDR);
    servaddr.sin_port = htons(LISTENING_PORT);

    if (bind(listenSockFd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("bind");
        return errno;
    }

    if (listen(listenSockFd, 5) < 0)
    {
        perror("listen");
        return errno;
    }

    return listenSockFd;
}

// File utils
int sendFile(char *file)
{
    msg_t msg;
    FILE  *fd;
    struct stat fInfo;

    memset(&msg, 0, sizeof(msg_t));
    memset(&fInfo, 0, sizeof(struct stat));

    fd = fopen(file, "r");
    if (!fd)
    {
        perror("fopen");
        return errno;
    }

    if (fstat(fileno(fd), &fInfo))
    {
        perror("fstat");
        return errno;
    }

    strcpy(msg.u.f.file, file);
    msg.u.f.size = fInfo.st_size;
    msg.u.f.chunk_size = fread(msg.u.f.data, 1, 1024, fd);
    while ((msg.u.f.offset + msg.u.f.chunk_size) <= msg.u.f.size) {
        if (sendMsg((char *)&msg, sizeof(msg_t)) != 0) {
            return errno;
        }
        if (msg.u.f.chunk_size < 1024) {
            if (feof(fd)) {
                LOG("Transmitted file: %s\n", msg.u.f.file);
                break;
            }
        }
        msg.u.f.offset += msg.u.f.chunk_size;
        msg.u.f.chunk_size = fread(msg.u.f.data, 1, 1024, fd);
    }
    return 0;
}

// primitive authentication - can be improved.... a LOT (FIXME fast!!)
int authStatus (char *u, char *p)
{
    int i = 0;
    while (users[i].u != NULL) {
        if ((strcmp(users[i].u, u) == 0) &&
            (strcmp(users[i].p, p) == 0)) {
            return 0;
        }
        i++;
    }
    return -1;
}

int executeCmd(msg_t *msg)
{
    msg_t newMsg;
    memset(&newMsg, 0, sizeof(newMsg));
    switch (msg->cmd)
    {
        case AUTH_REQ:
            if (authStatus(msg->u.a.user, msg->u.a.passwd) == 0)
            {
                newMsg.cmd = AUTH_RESP;
                strcpy(newMsg.u.a.user, "Success");
            }
            else
            {
                newMsg.cmd = AUTH_RESP;
                strcpy(newMsg.u.a.user, "Fail");
            }

            if (sendMsg((char *)&newMsg, sizeof(msg_t)) != 0) {
                return errno;
            }
            break;

        case FILE_REQ:
            if (sendFile(msg->u.f.file) != 0) {
                return FILE_RESP;
            }
            break;

        default:
            printf("Unknown command\n");
            exit(-1);
            break;
    }

    return 0;
}

int dispatcher()
{
    msg_t msg;
    socklen_t len;
    while (1) 
    {
        memset(&msg, 0, sizeof(msg));
        // Blocking select
        select(clntSockFd + 1, &readFds, NULL, NULL, NULL);

        if ((len = recvfrom(clntSockFd, &msg, sizeof(msg), 0, NULL, 0)) > 0) {
            LOG("Received msg with %d bytes\n", len);
            if (executeCmd(&msg) == FILE_RESP) {
                msg.cmd = FILE_RESP;
                msg.u.f.size = 0;
                sendMsg((char *)&msg, sizeof(msg_t));
            }
        }
        else {
            perror("recvfrom");
            break;
        }
    }

    close(clntSockFd);
    return 0;
}

int waitForClient()
{
    int buflen = 2048;
    socklen_t len;
    clntSockFd = accept(listenSockFd, (struct sockaddr *)&clnt, &len);
    if (clntSockFd < 0) 
    {
        perror("accept");
        return -1;
    }

    setsockopt(clntSockFd, SOL_SOCKET, SO_SNDBUF, &buflen, sizeof(int));
    setsockopt(clntSockFd, SOL_SOCKET, SO_RCVBUF, &buflen, sizeof(int));

    LOG("Accepted connection\n");

    FD_SET(clntSockFd, &readFds);
    return 0;
}

int main()
{
    if (createListeningSocket() < 0) {

        LOG("Socket creation failed - Exiting\n");
        return -1;
    }

    while(1) {

        FD_ZERO(&readFds);

        if (waitForClient() < 0) {
            LOG("accept failed\n");
            return -1;
        }

        dispatcher();
    }
}
